Лабораторная работа 16 "Оценка производительности"
Допуск к лабораторной работе
Данная лабораторная работа будет полностью опираться на навыки, полученные в ходе выполнения лабораторных работ:
Цель
Дать количественную оценку, характеризующую производительность реализованной вычислительной системы. На текущий момент мы создали процессорную систему, которая способна взаимодействовать с внешним миром посредством периферийных устройств ввода-вывода и программатора, по сути являющуюся компьютером. Однако встает вопрос, какое место данная система занимает в ряду уже существующих вычислительных систем.
Для оценки производительности необходимо модифицировать существующую процессорную систему, а после собрать и запустить специализированное ПО, отвечающее за измерение производительности (будет использована программа Coremark).
Теория
Coremark (далее кормарк) — это набор синтетических тестов (специальных программ) для измерения производительности процессорной системы. В данный набор входят такие тесты, как работа со связными списками, матричные вычисления, обработка конечных автоматов и подсчет контрольной суммы. Результат выражается в одном числе, которое можно использовать для сравнения с результатами других процессорных систем.
Для подсчета производительности, кормарк опирается на функцию, возвращающую текущее время, поэтому для оценки производительности нам потребуется вспомогательное периферийное устройство: таймер.
Для вывода результатов тестирования, необходимо описать способ, которым кормарк сможет выводить очередной символ сообщения — для этого мы будем использовать контроллер UART из предыдущих лабораторных работ.
Кроме того, скомпилированная без оптимизаций программа будет занимать чуть более 32KiB, поэтому нам потребуется изменить размер памяти инструкций.
Таким образом, для того чтобы запустить данную программу, нам необходимо выполнить как аппаратные изменения процессорной системы (добавить таймер и (если отсутствует) контроллер UART), так и программные изменения самого кормарка (для этого в нем предусмотрены специальные платформозависимые файлы, в которых объявлены функции, реализацию которых нам необходимо выполнить).
Задание
- Реализовать модуль-контроллер "таймер".
- Подключить этот модуль к системной шине. 2.1. В случае, если до этого в ЛР13 вашим устройством вывода было не UART TX, вам необходимо подключить к системной шине готовый модуль uart_tx_sb_ctrl.
- Добавить реализацию платформозависимых функций программы coremark.
- Скомпилировать программу.
- Изменить размер памяти инструкций.
- Запустить моделирование.
- Сравнить результаты измерения производительности с результатами существующих процессорных системам.
Таймер
Разберемся с тем, как будет работать наш таймер. По сути, это просто системный счетчик (не путайте с программным счетчиком), непрерывно считающий такты с момента последнего сброса. Системным он называется потому, что работает на системной тактовой частоте. Для измерения времени мы будем засекать значение счетчика на момент начала отсчета и значение счетчика в конце отсчета. Зная тактовую частоту и разность между значениями счетчика мы с легкостью сможем вычислить прошедшее время. При этом нужно обеспечить счетчик такой разрядностью, чтобы он точно не смог переполниться.
Поскольку мы уже назвали данный модуль "таймером", чтобы тот не был слишком простым, давайте добавим ему функциональности: пускай это будет устройство, способное генерировать прерывание через заданное число тактов. Таким образом, процессорная система сможет засекать время без постоянного опроса счетчика.
Было бы удобно, чтобы мы могли управлять тем, каким образом данный модуль будет генерировать такое прерывание: однократно, заданное число раз, или же бесконечно, пока тот не остановят.
Таким образом, мы сформировали следующее адресное пространство данного контроллера:
Адрес | Режим доступа | Допустимые значения | Функциональное назначение |
---|---|---|---|
0x00 | R | [0:2⁶⁴-1] | Значение системного счетчика, доступное только для чтения |
0x04 | RW | [0:2⁶⁴-1] | Указание задержки, спустя которую таймер будет генерировать прерывание |
0x08 | RW | [0:2] | Указание режима генерации прерываний (выключен, заданное число раз, бесконечно) |
0x0c | RW | [0:2³²-1] | Указание количества повторений генерации прерываний |
0x24 | W | 1 | Программный сброс |
Прототип модуля следующий:
module timer_sb_ctrl(
/*
Часть интерфейса модуля, отвечающая за подключение к системной шине
*/
input logic clk_i,
input logic rst_i,
input logic req_i,
input logic write_enable_i,
input logic [31:0] addr_i,
input logic [31:0] write_data_i, // не используется, добавлен для
// совместимости с системной шиной
output logic [31:0] read_data_o,
output logic ready_o,
/*
Часть интерфейса модуля, отвечающая за отправку запросов на прерывание
процессорного ядра
*/
output logic interrupt_request_o
);
Для работы данного контроллера потребуются следующие сигналы:
logic [63:0] system_counter;
logic [63:0] delay;
enum logic [1:0] {OFF, NTIMES, FOREVER} mode, next_mode;
logic [31:0] repeat_counter;
logic [63:0] system_counter_at_start;
system_counter
— регистр, ассоциированный с адресом0x00
, системный счетчик. Задача регистра заключается в ежетактном увеличении на единицу.delay
— регистр, ассоциированный с адресом0x04
. Число тактов, спустя которое таймер (когда тот будет включен) сгенерирует прерывание. Данный регистр изменяется только сбросом, либо запросом на запись.mode
— регистр, ассоциированный с адресом0x08
. Режим работы таймера:OFF
— отключен (не генерирует прерывания)NTIMES
— включен до тех пор, пока не сгенерирует N прерываний (Значение N хранится в регистреrepeat_counter
и обновляется после каждого сгенерированного прерывания). После генерации N прерываний, переходит в режимOFF
.FOREVER
— бесконечная генерация прерываний. Не отключится, пока режим работы прерываний не будет изменен.
next_mode
— комбинационный сигнал, который подается на вход записи в регистрmode
(аналогnext_state
из предыдущей лабораторной работы).repeat_counter
— регистр, ассоциированный с адресом0x0c
. Количество повторений для режимаNTIMES
. Уменьшается в момент генерации прерывания в этом режиме.system_counter_at_start
— неархитектурный регистр, хранящий значение системного счетчика на момент начала отсчета таймера. Обновляется при генерации прерывания (если это не последнее прерывание в режимеNTIMES
) и при запросе на запись в регистрmode
значения неOFF
.
Для подключения данного таймера к системной шине, мы воспользуемся первым свободным базовым адресом, оставшимся после ЛР13: 0x08
. Таким образом, для обращения к системному счетчику, процессор будет использовать адрес 0x08000000
для обращения к регистру delay
0x08000004
и т.п.
Настройка Coremark
В первую очередь, необходимо скачать исходный код данной программы, размещенный по адресу: https://github.com/eembc/coremark. На случай возможных несовместимых изменений в будущем, все дальнейшие ссылки будут даваться на слепок репозитория, который был на момент коммита d5fad6b
.
После этого, чтобы добавить поддержку нашей процессорной системы потребуется:
- Реализовать функцию, измеряющую время
- Реализовать функцию, выводящую очередной символ сообщения с результатами
- Реализовать функцию, выполняющую первичную настройку периферии перед тестом
- Выполнить мелкую подстройку, такую как количество итераций в тесте и указание аргументов, с которыми будет скомпилирована программа.
Все файлы, содержимое которых мы будем менять расположены в папке barebones.
1. Реализация функции, измеряющей время
Не мы первые придумали измерять время путем отсчета системных тактов, поэтому вся логика по измерению времени уже реализована в coremark. От нас требуется только реализовать функцию, которая возвращает текущее значение системного счетчика.
Данной функцией является barebones_clock
, расположенная в файле core_portme.c
. В данный момент, в реализации функции описан вызов ошибки (поскольку реализации как таковой нет). Мы должны заменить реализацию функции следующим кодом:
barebones_clock()
{
volatile ee_u32 *ptr = (ee_u32*)0x08000000;
ee_u32 tim = *ptr;
return tim;
}
После ЛР14 вы уже должны представлять, что здесь происходит. Мы создали указатель с абсолютным адресом 0x08000000
— адресом системного счетчика. Разыменование данного указателя вернет текущее значение системного счетчика, что и должно быть результатом вызова этой функции.
Для того, чтобы корректно преобразовать тики системного счетчика во время, используется функция time_in_secs
, которая уже реализована, но для работы которой нужно определить макрос CLOCKS_PER_SEC
, характеризующий тактовую частоту, на которой работает процессор. Давайте определим данный макрос сразу над макросом EE_TICKS_PER_SEC
:
#define CLOCKS_PER_SEC 10000000
На этом наша задача по измерению времени завершена. Остальные правки будут не сложнее этих.
2. Реализация вывода очередного символа сообщения
Для вывода очередного символа во встраиваемых системах используется (какое совпадение!) функция uart_send_char
, расположенная в файле ee_printf.c
.
В реализации данной функции вам уже предлагают алгоритм, по которому та должна работать. Необходимо:
- дождаться готовности UART к отправке;
- передать отправляемый символ;
- дождаться готовности UART к отправке (завершения передачи).
Давайте так и реализуем эту функцию:
uart_send_char(char c)
{
volatile ee_u8 *uart_ptr = (ee_u8 *)0x06000000;
while(*(uart_ptr+0x08));
*uart_ptr = c;
while(*(uart_ptr+0x08));
}
0x06000000
— базовый адрес контроллера UART TX из ЛР13 (и адрес передаваемых этим контроллером данных).
0x08
— смещение до адреса регистра busy
в адресном пространстве этого контроллера.
3. Реализация функции первичной настройки
Это функция portable_init
, расположена в уже известном ранее файле [core_portme
.c]. Данная функция выполняет необходимые нам настройки перед началом теста. Для нас главное — настроить нужным образом контроллер UART.
Допустим, мы хотим чтобы данные передавались на скорости 115200
, c одним стоповым битом и контролем бита четности. В этом случае, мы должны добавить в начало функции следующий код:
portable_init(core_portable *p, int *argc, char *argv[])
{
volatile ee_u32 *uart_tx_ptr = (ee_u32 *)0x06000000;
*(uart_tx_ptr + 3) = 115200;
*(uart_tx_ptr + 4) = 1;
*(uart_tx_ptr + 5) = 1;
//...
}
4. Дополнительные настройки
Для тонких настроек используется заголовочный файл core_portme.h
, куда также требуется внести несколько изменений. Нам необходимо:
- Объявить в начале файла макрос
ITERATIONS
, влияющий на количество прогонов теста. Нам достаточно выставить значение 1. - Обновить значение макроса
COMPILER_FLAGS
, заменив его значениеFLAGS_STR
на"-march=rv32i_zicsr -mabi=ilp32"
, именно с этими аргументами мы будем собирать программу. Это опциональная настройка, которая позволит вывести флаги компиляции в итоговом сообщении. - Добавить подключение заголовочного файла
#include <stddef.h>
.
Компиляция
Для компиляции программы, вам потребуются предоставленные файлы Makefile и linker_script.ld, а также файл startup.S из ЛР14. Эти файлы необходимо скопировать с заменой в корень папки с программой.
Makefile
написан из расчёта, что кросс-компилятор расположен по пути C:/riscv_cc/
. В случае, если это не так, измените первую строчку данного файла в соответствии с расположением кросс-компилятора.
Для запуска компиляции, необходимо выполнить следующую команду, находясь в корне программы coremark:
make
В случае, если на вашем рабочем компьютере не установлена утилита make
, то вы можете скомпилировать программу вручную, выполнив следующую последовательность команд:
cp barebones/*.c barebones/*.h ./
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_main.c -o core_main.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" startup.S -o startup.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_list_join.c -o core_list_join.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_matrix.c -o core_matrix.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_portme.c -o core_portme.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_state.c -o core_state.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" core_util.c -o core_util.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" cvt.c -o cvt.o
/c/riscv_cc/bin/riscv-none-elf-gcc -c -march=rv32i_zicsr -mabi=ilp32 -I"./" ee_printf.c -o ee_printf.o
/c/riscv_cc/bin/riscv-none-elf-gcc core_main.o startup.o core_list_join.o core_matrix.o core_portme.o core_state.o core_util.o cvt.o ee_printf.o -Wl,--gc-sections -nostartfiles -T linker_script.ld -march=rv32i_zicsr -mabi=ilp32 -I"./" -o coremark.elf
/c/riscv_cc/bin/riscv-none-elf-objdump -D coremark.elf > coremark_disasm.S
/c/riscv_cc/bin/riscv-none-elf-objcopy -O verilog --verilog-data-width=4 -j .data -j .sdata -j .bss coremark.elf coremark_data.mem
/c/riscv_cc/bin/riscv-none-elf-objcopy -O verilog --verilog-data-width=4 -j .text coremark.elf coremark_instr.mem
/c/riscv_cc/bin/riscv-none-elf-size coremark.elf
sed -i '1d' coremark_data.mem
В случае успешной компиляции, вам будет выведено сообщение об итоговом размере секций инструкций и данных:
text data bss dec hex filename
34324 2268 100 36692 8f54 coremark.elf
Изменение размера памяти инструкций
Как видите, размер секции инструкций превышает 32KiB на 1556 байт (32768—34324). Поэтому на время оценки моделирования, нам придется увеличить размер памяти инструкций до 64KiB, изменив число слов памяти инструкций до 16384. При этом необходимо изменить диапазон бит адреса, используемых для чтения инструкции из памяти с [11:2]
на [15:2]
.
Обратите внимание, что увеличение размера памяти в 16 раз приведет к значительному увеличению времени синтеза устройства, поэтому данное изменение мы производим исключительно на время поведенческого моделирования.
Запуск моделирования
Программирование 32KiB по UART займет ощутимое время, поэтому вам предлагается проинициализировать память инструкций и данных "по старинке" через системные функции $readmemh
.
Если все было сделано без ошибок, то примерно на 276ms
времени моделирования вам начнется выводиться сообщение вида:
CoreMark Size : 666
Total ticks : 2574834
Total time (secs): <скрыто то получения результатов моделирования>
Iterations/Sec : <скрыто то получения результатов моделирования>
ERROR! Must execute for at least 10 secs for a valid result!
Iterations : 1
Compiler version : GCC13.2.0
Compiler flags : -march=rv32i_zicsr -mabi=ilp32
Memory location : STACK
seedcrc : 0x29f4
[0]crclist : 0x7704
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0x7704
Correct operation validated. See README.md for run and reporting rules.
(вывод сообщения будет завершен приблизительно на 335ms
времени моделирования).
Порядок выполнения задания
- Опишите таймер в виде модуля
timer_sb_ctrl
. - Проверьте описанный модуль с помощью тестового окружения tb_timer.
- Подключите
timer_sb_ctrl
к системной шине. Сигнал прерывания этого модуля подключать не нужно. 2.1 В случае, если до этого в ЛР13 вашим устройством вывода было не UART TX, вам необходимо подключить к системной шине и готовый модуль uart_tx_sb_ctrl. - Получите исходники программы Coremark. Для этого можно либо склонировать репозиторий, либо скачать его в виде архива со страницы: https://github.com/eembc/coremark.
- Добавьте реализацию платформозависимых функций программы coremark. Для этого в папке
barebones
необходимо:- в файле
core_portme.c
:- реализовать функцию
barebones_clock
, возвращающую текущее значение системного счетчика; - объявить макрос
CLOCKS_PER_SEC
, характеризующий тактовую частоту процессора; - реализовать функцию
portable_init
, выполняющую первичную инициализацию периферийных устройств до начала теста;
- реализовать функцию
- в файле
ee_printf.c
реализовать функциюuart_send_char
, отвечающую за отправку очередного символа сообщения о результате.
- в файле
- Добавьте с заменой в корень программы файлы Makefile, linker_script.ld и startup.S.
- Скомпилируйте программу вызовом
make
.- Если кросскомпилятор расположен не в директории
C:/riscv_cc
, перед вызовомmake
вам необходимо соответствующим образом отредактировать первую строчку вMakefile
. - В случае отсутствия на компьютере утилиты
make
, вы можете самостоятельно скомпилировать программу вызовом команд, представленных в разделе "Компиляция".
- Если кросскомпилятор расположен не в директории
- Временно измените размер памяти инструкций до 64KiB.
- Для этого необходимо изменить размер памяти инструкций с 1024 слов до 16384 слов.
- Кроме того, необходимо изменить используемые индексы адреса в памяти с
[11:2]
на[15:2]
.
- Проинициализируйте память инструкций и память данных файлами "coremark_instr.mem" и "coremark_data.mem", полученными в ходе компиляции программы.
- Выполните моделирование системы с помощью модуля tb_coremark.
- Результаты теста будут выведены приблизительно на
335ms
времени моделирования.
- Результаты теста будут выведены приблизительно на
11. Прочти меня после успешного завершения моделирования
Итак, вы получили сообщение вида:
CoreMark Size : 666
Total ticks : 2574834
Total time (secs): 0.257483
Iterations/Sec : 3.883746
ERROR! Must execute for at least 10 secs for a valid result!
Iterations : 1
Compiler version : GCC13.2.0
Compiler flags : -march=rv32i_zicsr -mabi=ilp32
Memory location : STACK
seedcrc : 0x29f4
[0]crclist : 0x7704
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0x7704
Correct operation validated. See README.md for run and reporting rules.
Не обращайте внимание на строчку "ERROR! Must execute for at least 10 secs for a valid result!". Программа считает, что для корректных результатов, необходимо крутить ее по кругу в течении минимум 10 секунд, однако по большей части это требование необходимо для более достоверного результата у систем с кэшем/предсказателями переходов и прочими блоками, которые могут изменить количество тактов на прохождение между итерациями. Наш однотактный процессор будет вести себя одинаково на каждом круге, поэтому нет смысла в дополнительном времени моделирования.
Нас интересует строка:
Iterations/Sec : 3.883746
Это и есть так называемый "кормарк" — метрика данной программы. Результат нашего процессора: 3.88 кормарка.
Обычно, для сравнения между собой нескольких реализаций микроархитектур, более достоверной считается величина "кормарк / МГц", т.е. значение кормарка, поделённое на тактовую частоту процессора. Дело в том, что можно реализовать какую-нибудь очень сложную архитектуру, которая будет выдавать очень хороший кормарк, но при этом будет иметь очень низкую частоту. Более того, при сравнении с другими результатами, необходимо учитывать флаги оптимизации, которые использовались при компиляции программы, поскольку они также влияют на результат.
Мы не будем уходить в дебри темных паттернов маркетинга и вместо этого будет оценивать производительность в лоб: сколько кормарков в секунду смог прогнать наш процессор в сравнении с представленными результатами других систем вне зависимости от их оптимизаций.
Таблица опубликованных результатов находится по адресу: https://www.eembc.org/coremark/scores.php. Нам необходимо отсортировать эту таблицу по столбцу CoreMark
, кликнув по нему.
Мы получим следующий расклад:
На что мы можем обратить внимание? Ну, во-первых, мы видим, что ближайший к нам микроконтроллер по кормарку — это ATmega2560
с результатом 4.25
кормарка. Т.е. наш процессор по производительности схож с микроконтроллерами Arduino.
Есть ли здесь еще что-нибудь интересное? Посмотрим в верх таблицы, мы можем увидеть производителя Intel с их микропроцессором Intel 80286. Как написано на вики, данный микропроцессор был в 3-6 раз производительней Intel 8086, который соперничал по производительности с процессором Zilog Z80, который устанавливался в домашний компьютер TRS-80.
А знаете, с чем был сопоставим по производительности компьютер TRS-80? С бортовым компьютером Apollo Guidance Computer, который проводил вычисления и контролировал движение, навигацию, управлял командным и лунным модулями в ходе полётов по программе Аполлон.
Иными словами, мы разработали процессор, который приблизительно в 7-14 раз производительнее компьютера, управлявшего полетом космического корабля, который доставил человека на Луну!
Можно ли как-то улучшить наш результат? Безусловно. Мы можем улучшить его примерно на 5% изменив буквально одну строчку. Дело в том, что для простоты реализации, мы генерировали сигнал stall
для каждой операции обращения в память. Однако приостанавливать работу процессора было необходимо только для операций чтения из памяти. Если не генерировать сигнал stall
для операций типа store
, мы уменьшим время, необходимое на исполнение бенчмарка. Попробуйте сделать это сами.
Добавление умножителей, конвейеризация и множество других потенциальных улучшений увеличат производительность в разы.
Но это, как говорится, уже другая история.